﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;

namespace Heimdallr.Net
{
    /// <summary>
    /// Represents the notification mechanism base on HTTP long polling.
    /// </summary>
    public class HttpPollingResponseModule : HttpModule, IHttpResponseModule
    {
        #region Field

        /// <summary>
        /// Represents the default value of <see cref="HttpModule.RelativePath"/> property.
        /// This field is read-only.
        /// </summary>
        public readonly static string DefaultRelativePath = "svc-bin/polling/";

        private readonly Queue<HttpResponseContext> waitings;
        private readonly Encoding encoding;
        private readonly WebHeaderCollection emptyResponseHeaders;
        private readonly Timer timer;
        private readonly object syncLock;

        private int duration;
        private HttpStatusCode emptyResponseStatusCode;

        #endregion

        #region Property

        /// <summary>
        /// Gets or sets the maximum time, in seconds, of response waiting. 
        /// The default value is 5.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">Value can't below or equal zero.</exception>
        public int Duration
        {
            get { return this.duration; }
            set
            {
                if (value <= 0)
                    throw new ArgumentOutOfRangeException("Duration", "Value can't below or equal zero.");

                this.duration = value;
                this.timer.Change(TimeSpan.FromSeconds(this.duration), TimeSpan.FromSeconds(this.duration));
            }
        }

        /// <summary>
        /// Gets or sets the HTTP status code of emtpy response if there is no notification while response waiting.
        /// The default value is <see cref="HttpStatusCode">HttpStatusCode.NoContent</see>.
        /// </summary>
        public HttpStatusCode EmptyResponseStatusCode
        {
            get { return this.emptyResponseStatusCode; }
            set { this.emptyResponseStatusCode = value; }
        }

        /// <summary>
        /// Gets a collection of HTTP response headers for emtpy response if there is no notification while waiting.
        /// </summary>
        public WebHeaderCollection EmptyResponseHeaders
        {
            get { return this.emptyResponseHeaders; }
        }

        /// <summary>
        /// Gets a collection of registered HTTP response contexts which are waiting for notifications.
        /// </summary>
        public IEnumerable<HttpResponseContext> RegisteredResponses
        {
            get { return this.waitings; }
        }

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance with HTTP <see cref="WebRequestMethods.Http.Get">GET</see> method and <see cref="DefaultRelativePath"/>.
        /// </summary>
        public HttpPollingResponseModule()
            : this(WebRequestMethods.Http.Get, DefaultRelativePath, HttpModuleCredentialType.None)
        {
        }

        /// <summary>
        /// Initializes a new instance with HTTP <see cref="WebRequestMethods.Http.Get">GET</see> method and indicated path.
        /// </summary>
        /// <param name="relativePath">HTTP relative path.</param>
        /// <exception cref="ArgumentException">Parameter <paramref name="relativePath"/> must end with character '/'.</exception>
        public HttpPollingResponseModule(string relativePath)
            : this(WebRequestMethods.Http.Get, relativePath, HttpModuleCredentialType.None)
        {
        }

        /// <summary>
        /// Initializes a new instance with HTTP <see cref="WebRequestMethods.Http.Get">GET</see> method, indicated path and credential type.
        /// </summary>
        /// <param name="relativePath">HTTP relative path.</param>
        /// <param name="credentialType">HTTP credential type.</param>
        /// <exception cref="ArgumentNullException">Parameter <paramref name="relativePath"/> is null.</exception>
        /// <exception cref="ArgumentException">Parameter <paramref name="relativePath"/> must end with character '/'.</exception>
        /// <exception cref="InvalidEnumArgumentException">Parameter <paramref name="credentialType"/> is not a valid value.</exception>
        public HttpPollingResponseModule(string relativePath, HttpModuleCredentialType credentialType)
            : this(WebRequestMethods.Http.Get, relativePath, credentialType)
        {
        }

        /// <summary>
        /// Initializes a new instance with indicated HTTP method, path and credential type.
        /// </summary>
        /// <param name="method">HTTP method.</param>
        /// <param name="relativePath">HTTP relative path.</param>
        /// <param name="credentialType">HTTP credential type.</param>
        /// <exception cref="ArgumentNullException">Parameter <paramref name="method"/> or <paramref name="relativePath"/> is null.</exception>
        /// <exception cref="ArgumentException">Parameter <paramref name="relativePath"/> must end with character '/'.</exception>
        /// <exception cref="InvalidEnumArgumentException">Parameter <paramref name="credentialType"/> is not a valid value.</exception>
        public HttpPollingResponseModule(string method, string relativePath, HttpModuleCredentialType credentialType)
            : base(method, relativePath, credentialType)
        {
            this.waitings = new Queue<HttpResponseContext>();
            this.encoding = Encoding.UTF8;
            this.duration = 5;
            this.timer = new Timer(this.EmptyResponseToAllCallback, null, TimeSpan.FromSeconds(this.duration), TimeSpan.FromSeconds(this.duration));
            this.syncLock = new object();
            this.emptyResponseStatusCode = HttpStatusCode.NoContent;
            this.emptyResponseHeaders = new WebHeaderCollection();
        }

        #endregion

        #region HttpModule Member

        /// <summary>
        /// When implemented in a derived class, indicates if user name and password of incoming request is valid or not.
        /// </summary>
        /// <param name="username">User name of incoming request.</param>
        /// <param name="password">Password of incoming request.</param>
        /// <returns>If valid, the return value is true. Otherwise false.</returns>
        public override bool Validate(string username, string password)
        {
            return true;
        }

        /// <summary>
        /// When implemented in a derived class, performs tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public override void Dispose()
        {
            foreach (var waiting in this.waitings)
                waiting.Dispose();

            this.timer.Dispose();
        }

        #endregion

        #region IHttpResponseModule Member

        /// <summary>
        /// Registers an alive response previously which has prepared for notifying.
        /// </summary>
        /// <param name="response">An alive response created and passed by <see cref="HttpElement"/>.</param>
        /// <returns>If success, return true. Otherwise return false.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="response"/> is null.</exception>
        public virtual bool RegisterContext(HttpResponseContext response)
        {
            if (response == null)
                throw new ArgumentNullException("response");

            lock (this.syncLock)
                this.waitings.Enqueue(response);

            this.ContextRegistered(response);
#if TRACE
            Trace.TraceInformation("Current response {0} waiting.", this.waitings.Count(r => !r.IsDisposed));
#endif
            return true;
        }

        /// <summary>
        /// Publishes a notification to all registered <see cref="HttpResponseContext"/> instances.
        /// </summary>
        /// <param name="notification">A notification which is going to be published.</param>
        /// <exception cref="ArgumentNullException"><paramref name="notification"/> is null.</exception>
        public virtual void Publish(Notification notification)
        {
            if (notification == null)
                throw new ArgumentNullException("notification");
#if TRACE
            Trace.TraceInformation("Ready to send {0} notification response(s).", this.waitings.Count(r => !r.IsDisposed));
#endif
            lock (this.syncLock)
            {
                var actions = from w in this.waitings
                              where !w.IsDisposed
                              select new PublishAction(w, notification);

                foreach (PublishAction action in actions)
                    ThreadPool.QueueUserWorkItem(this.NotificationResponseCallback, action);
            }
        }

        #endregion

        #region Callback

        private void NotificationResponseCallback(object state)
        {
            PublishAction p = state as PublishAction;
            Notification n = p.Notification;
            object userState = this.BeforePublish(p.Response.Description, ref n);

            if (n == null)  // The publish action has been cancelled by derived class.  
                return;

            using (HttpResponseContext response = p.Response)
            {
                Exception error = null;

                if (!TrySetResponseHeaders(response, n, out error))
                {
                    this.AfterPublish(response.Description, error, userState);
                    this.ContextUnregistered(response);

                    return;
                }

                try
                {
                    using (Stream src = n.CreateReadOnlyStream(65536))
                    using (Stream rsp = response.GetResponseStream())
                        src.CopyTo(rsp, 65536);
                }
                catch (Exception e)
                {
                    error = e;
                }

                this.AfterPublish(response.Description, error, userState);
                this.ContextUnregistered(response);
            }
        }

        private void EmptyResponseToAllCallback(object state)
        {
            if (!Monitor.TryEnter(this.syncLock))
                return;
#if TRACE
            Trace.TraceInformation("{0} empty response(s) sent.", this.waitings.Count(r => !r.IsDisposed));
#endif
            while (this.waitings.Any())
            {
                HttpResponseContext response = this.waitings.Dequeue();

                if (!response.IsDisposed)
                    ThreadPool.QueueUserWorkItem(this.EmptyResponseCallback, response);
            }

            Monitor.Exit(this.syncLock);
        }

        private void EmptyResponseCallback(object state)
        {
            using (HttpResponseContext response = state as HttpResponseContext)
                try
                {
                    response.StatusCode = this.emptyResponseStatusCode;
                    TrySetResponseHeaders(response, this.emptyResponseHeaders);
                }
#if DEBUG
                catch (Exception e)
                {
                    Debug.WriteLine(e);
                }
#else
                catch { }
#endif
                finally
                {
                    this.ContextUnregistered(response);
                }
        }

        #endregion

        #region Protected Method

        /// <summary>
        /// When implemented in a derived class, executes before a response is registered.
        /// </summary>
        /// <param name="response">A response context that is ready for being registered.</param>
        protected virtual void ContextRegistered(HttpResponseContext response)
        {

        }

        /// <summary>
        /// When implemented in a derived class, executes after a response has unregistered.
        /// </summary>
        /// <param name="response">A response context that has been unregistered.</param>
        protected virtual void ContextUnregistered(HttpResponseContext response)
        {

        }

        /// <summary>
        /// When implemented in a derived class, executes before publishing a notification. 
        /// </summary>
        /// <param name="description">An object that provides informations of current response context.</param>
        /// <param name="notification">A notification which is ready for publishing.</param>
        /// <returns>
        /// The object that is returned as the userState argument of the <see cref="AfterPublish"/> method. 
        /// This is null if no correlation state is used.
        /// </returns>
        protected virtual object BeforePublish(HttpOperationDescription description, ref Notification notification)
        {
            return null;
        }

        /// <summary>
        /// When implemented in a derived class, executes after publishing a notification. 
        /// </summary>
        /// <param name="description">An object that provides informations of current response context.</param>
        /// <param name="error">The error that occurs while publishing a notification.</param>
        /// <param name="userState">The returned value of <see cref="BeforePublish"/> method.</param>
        protected virtual void AfterPublish(HttpOperationDescription description, Exception error, object userState)
        {

        }

        #endregion

        #region Misc

        private static bool TrySetResponseHeaders(HttpResponseContext response, Notification n, out Exception error)
        {
            HttpStatusCode responseStatusCode;
            NameValueCollection headers;
            HttpResponseNotification httpRespN = n as HttpResponseNotification;

            if (httpRespN != null)
            {
                responseStatusCode = httpRespN.StatusCode;
                headers = httpRespN.Headers;
            }
            else
            {
                responseStatusCode = HttpStatusCode.OK;
                headers = null;
            }

            try
            {
                response.StatusCode = responseStatusCode;
                response.ContentType = n.ContentType;
                response.ContentLength = n.ContentLength;
            }
            catch (Exception e)
            {
                error = e;

                return false;
            }

            if (headers != null && headers.Count > 0)
                TrySetResponseHeaders(response, headers);

            error = null;

            return true;
        }

        private static void TrySetResponseHeaders(HttpResponseContext response, NameValueCollection headers)
        {
            foreach (string key in headers.AllKeys)
            {
                try
                {
                    response.Headers[key] = headers[key];
                }
#if DEBUG
                catch (Exception e) // Ignore it.
                {
                    Debug.WriteLine(e);
                }
#else
                catch { }
#endif
            }
        }

        #endregion

        #region Other

        internal class PublishAction
        {
            #region Field

            private readonly HttpResponseContext action;
            private readonly Notification n;

            #endregion

            #region Property

            public HttpResponseContext Response
            {
                get { return this.action; }
            }

            public Notification Notification
            {
                get { return this.n; }
            }

            #endregion

            #region Constructor

            public PublishAction(HttpResponseContext action, Notification n)
            {
                this.action = action;
                this.n = n;
            }

            #endregion
        }

        #endregion
    }
}